﻿
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using StackExchange.Redis;

namespace StarUtils.Cache
{
    public class RedisCacheForSession : IDistributedCache, IDisposable
    {
        private const string SetScript = "\r\n                redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])\r\n                if ARGV[3] ~= '-1' then\r\n                  redis.call('EXPIRE', KEYS[1], ARGV[3])\r\n                end\r\n                return 1";

        private const string AbsoluteExpirationKey = "absexp";

        private const string SlidingExpirationKey = "sldexp";

        private const string DataKey = "data";

        private const long NotPresent = -1L;

        private readonly IRedisDatabaseProvider _dbProvider;

        private IDatabase _cache;

        private readonly RedisOptions _options;

        private readonly string _instance;

        private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1, 1);

        public RedisCacheForSession(IRedisDatabaseProvider dbProvider, IOptionsMonitor<RedisOptions> optionsAccessor)
        {
            if (optionsAccessor == null)
            {
                throw new ArgumentNullException("optionsAccessor");
            }

            _dbProvider = dbProvider;
            _options = optionsAccessor.CurrentValue;
            _instance = _options.PrefixName ?? string.Empty;
        }

        public byte[] Get(string key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            return GetAndRefresh(key, getData: true);
        }

        public async Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken))
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            token.ThrowIfCancellationRequested();
            return await GetAndRefreshAsync(key, getData: true, token);
        }

        public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            Connect();
            DateTimeOffset utcNow = DateTimeOffset.UtcNow;
            DateTimeOffset? absoluteExpiration = GetAbsoluteExpiration(utcNow, options);
            _cache.ScriptEvaluate("\r\n                redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])\r\n                if ARGV[3] ~= '-1' then\r\n                  redis.call('EXPIRE', KEYS[1], ARGV[3])\r\n                end\r\n                return 1", new RedisKey[1] { _instance + key }, new RedisValue[4]
            {
                absoluteExpiration?.Ticks ?? (-1),
                options.SlidingExpiration?.Ticks ?? (-1),
                GetExpirationInSeconds(utcNow, absoluteExpiration, options) ?? (-1),
                value
            });
        }

        public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken))
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            token.ThrowIfCancellationRequested();
            await ConnectAsync(token);
            DateTimeOffset utcNow = DateTimeOffset.UtcNow;
            DateTimeOffset? absoluteExpiration = GetAbsoluteExpiration(utcNow, options);
            await _cache.ScriptEvaluateAsync("\r\n                redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])\r\n                if ARGV[3] ~= '-1' then\r\n                  redis.call('EXPIRE', KEYS[1], ARGV[3])\r\n                end\r\n                return 1", new RedisKey[1] { _instance + key }, new RedisValue[4]
            {
                absoluteExpiration?.Ticks ?? (-1),
                options.SlidingExpiration?.Ticks ?? (-1),
                GetExpirationInSeconds(utcNow, absoluteExpiration, options) ?? (-1),
                value
            });
        }

        public void Refresh(string key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            GetAndRefresh(key, getData: false);
        }

        public async Task RefreshAsync(string key, CancellationToken token = default(CancellationToken))
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            token.ThrowIfCancellationRequested();
            await GetAndRefreshAsync(key, getData: false, token);
        }

        private void Connect()
        {
            if (_cache != null)
            {
                return;
            }

            _connectionLock.Wait();
            try
            {
                if (_cache == null)
                {
                    _cache = _dbProvider.GetDatabase();
                }
            }
            finally
            {
                _connectionLock.Release();
            }
        }

        private async Task ConnectAsync(CancellationToken token = default(CancellationToken))
        {
            token.ThrowIfCancellationRequested();
            if (_cache != null)
            {
                return;
            }

            await _connectionLock.WaitAsync(token);
            try
            {
                if (_cache == null)
                {
                    _cache = _dbProvider.GetDatabase();
                }
            }
            finally
            {
                _connectionLock.Release();
            }
        }

        private byte[] GetAndRefresh(string key, bool getData)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            Connect();
            RedisValue[] array = ((!getData) ? _cache.HashMemberGet(_instance + key, "absexp", "sldexp") : _cache.HashMemberGet(_instance + key, "absexp", "sldexp", "data"));
            if (array.Length >= 2)
            {
                MapMetadata(array, out var absoluteExpiration, out var slidingExpiration);
                Refresh(key, absoluteExpiration, slidingExpiration);
            }

            if (array.Length >= 3 && array[2].HasValue)
            {
                return array[2];
            }

            return null;
        }

        private async Task<byte[]> GetAndRefreshAsync(string key, bool getData, CancellationToken token = default(CancellationToken))
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            token.ThrowIfCancellationRequested();
            await ConnectAsync(token);
            RedisValue[] results = ((!getData) ? (await _cache.HashMemberGetAsync(_instance + key, "absexp", "sldexp")) : (await _cache.HashMemberGetAsync(_instance + key, "absexp", "sldexp", "data")));
            if (results.Length >= 2)
            {
                MapMetadata(results, out var absoluteExpiration, out var slidingExpiration);
                await RefreshAsync(key, absoluteExpiration, slidingExpiration, token);
            }

            if (results.Length >= 3 && results[2].HasValue)
            {
                return results[2];
            }

            return null;
        }

        public void Remove(string key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            Connect();
            _cache.KeyDelete(_instance + key);
        }

        public async Task RemoveAsync(string key, CancellationToken token = default(CancellationToken))
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            await ConnectAsync(token);
            await _cache.KeyDeleteAsync(_instance + key);
        }

        private void MapMetadata(RedisValue[] results, out DateTimeOffset? absoluteExpiration, out TimeSpan? slidingExpiration)
        {
            absoluteExpiration = null;
            slidingExpiration = null;
            long? num = (long?)results[0];
            if (num.HasValue && num.Value != -1)
            {
                absoluteExpiration = new DateTimeOffset(num.Value, TimeSpan.Zero);
            }

            long? num2 = (long?)results[1];
            if (num2.HasValue && num2.Value != -1)
            {
                slidingExpiration = new TimeSpan(num2.Value);
            }
        }

        private void Refresh(string key, DateTimeOffset? absExpr, TimeSpan? sldExpr)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            TimeSpan? timeSpan = null;
            if (sldExpr.HasValue)
            {
                if (absExpr.HasValue)
                {
                    TimeSpan timeSpan2 = absExpr.Value - DateTimeOffset.Now;
                    timeSpan = ((timeSpan2 <= sldExpr.Value) ? new TimeSpan?(timeSpan2) : sldExpr);
                }
                else
                {
                    timeSpan = sldExpr;
                }

                _cache.KeyExpire(_instance + key, timeSpan);
            }
        }

        private async Task RefreshAsync(string key, DateTimeOffset? absExpr, TimeSpan? sldExpr, CancellationToken token = default(CancellationToken))
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            token.ThrowIfCancellationRequested();
            if (sldExpr.HasValue)
            {
                TimeSpan? expiry;
                if (absExpr.HasValue)
                {
                    TimeSpan timeSpan = absExpr.Value - DateTimeOffset.Now;
                    expiry = ((timeSpan <= sldExpr.Value) ? new TimeSpan?(timeSpan) : sldExpr);
                }
                else
                {
                    expiry = sldExpr;
                }

                await _cache.KeyExpireAsync(_instance + key, expiry);
            }
        }

        private static long? GetExpirationInSeconds(DateTimeOffset creationTime, DateTimeOffset? absoluteExpiration, DistributedCacheEntryOptions options)
        {
            if (absoluteExpiration.HasValue && options.SlidingExpiration.HasValue)
            {
                return (long)Math.Min((absoluteExpiration.Value - creationTime).TotalSeconds, options.SlidingExpiration.Value.TotalSeconds);
            }

            if (absoluteExpiration.HasValue)
            {
                return (long)(absoluteExpiration.Value - creationTime).TotalSeconds;
            }

            if (options.SlidingExpiration.HasValue)
            {
                return (long)options.SlidingExpiration.Value.TotalSeconds;
            }

            return null;
        }

        private static DateTimeOffset? GetAbsoluteExpiration(DateTimeOffset creationTime, DistributedCacheEntryOptions options)
        {
            if (options.AbsoluteExpiration.HasValue && options.AbsoluteExpiration <= creationTime)
            {
                throw new ArgumentOutOfRangeException("AbsoluteExpiration", options.AbsoluteExpiration.Value, "The absolute expiration value must be in the future.");
            }

            DateTimeOffset? result = options.AbsoluteExpiration;
            if (options.AbsoluteExpirationRelativeToNow.HasValue)
            {
                DateTimeOffset value = creationTime;
                TimeSpan? absoluteExpirationRelativeToNow = options.AbsoluteExpirationRelativeToNow;
                result = value + absoluteExpirationRelativeToNow;
            }

            return result;
        }

        public void Dispose()
        {
        }
    }
}
